package fssystem

import (
	"GLibHac/tools"
	"encoding/binary"
	"github.com/pkg/errors"
	"io"
	"math"
	"os"
	"strings"
)

type IStorage interface {
	Position() int64
	ReadInt() (int32, error)
	ReadBytes(length int) ([]byte, error)
	ReadUtf8Z(i int) (string, error)
	SetPosition(i int64) error
	ReadByte() (byte, error)
	ReadUint16() (uint16, error)
	ReadUint32() (uint32, error)
	ReadUint64() (uint64, error)
	ReadInt16() (int16, error)
	AsStorage() IStorage
	ReadInt64() (int64, error)
	ReadAsciiZ() (string, error)
	SliceRange(start, size int64) IStorage
	Slice(start int64) IStorage
	ReadUTF8(length int) (string, error)
	Read(position int64, destination []byte) (int, error)
	Size() int64
	ReadAll() ([]byte, error)
	ReadAllToInt() ([]int32, error)
	ReadUint64s(length int) ([]uint64, error)
	ReadUint16s(length int) ([]uint16, error)
}

type IStorageAbstract struct {
	position int64
	ReadFunc func(position int64, destination []byte) (int, error)
	SizeFunc func() int64
}

func (i *IStorageAbstract) Read(position int64, destination []byte) (int, error) {
	return i.ReadFunc(position, destination)
}

func (i *IStorageAbstract) Size() int64 {
	return i.SizeFunc()
}

func (i *IStorageAbstract) ReadUint16s(length int) ([]uint16, error) {
	bytes, err := i.ReadBytes(length * 2)
	if err != nil {
		return nil, err
	}
	result := make([]uint16, length)
	for i := 0; i < length; i++ {
		result[i] = binary.LittleEndian.Uint16(bytes[i*2:])
	}
	return result, nil
}

func (i *IStorageAbstract) ReadUint64s(length int) ([]uint64, error) {
	bytes, err := i.ReadBytes(length * 8)
	if err != nil {
		return nil, err
	}
	result := make([]uint64, length)
	for i := 0; i < length; i++ {
		result[i] = binary.LittleEndian.Uint64(bytes[i*8:])
	}
	return result, nil
}

func (i *IStorageAbstract) ReadAll() ([]byte, error) {
	cache := make([]byte, i.Size())
	if _, err := i.Read(0, cache); err != nil {
		return nil, err
	}
	return cache, nil
}

func (i *IStorageAbstract) ReadAllToInt() ([]int32, error) {
	err := i.SetPosition(0)
	if err != nil {
		return nil, err
	}
	all, err := i.ReadAll()
	if err != nil {
		return nil, err
	}
	l := len(all) / 4
	result := make([]int32, l)
	for i := 0; i < l; i++ {
		result[i] = tools.BytesToIntLittleEndian(all[i*4:])
	}
	return result, nil
}

func (i *IStorageAbstract) SliceRange(start, size int64) IStorage {
	return NewSubStorage(i, start, size)
}

func (i *IStorageAbstract) Slice(start int64) IStorage {
	return NewSubStorage(i, start, i.Size()-start)
}

func (i *IStorageAbstract) Position() int64 {
	return i.position
}

func (i *IStorageAbstract) ReadInt() (int32, error) {
	bytes, err := i.ReadBytes(4)
	if err != nil {
		return 0, err
	}
	return tools.BytesToIntLittleEndian(bytes), nil
}

func (i *IStorageAbstract) ReadBytes(length int) ([]byte, error) {
	data := make([]byte, length)
	n, err := i.Read(i.position, data)
	if err != nil {
		return nil, err
	}
	if n != length {
		return data[:n], io.EOF
	}
	return data, i.SetPosition(i.position + int64(length))
}

func (i *IStorageAbstract) ReadUtf8Z(maxLength int) (string, error) {
	p := i.Position()
	sb := strings.Builder{}
	// Read until we hit the end of the stream (-1) or a zero
	for {
		b, err := i.ReadByte()
		if err != nil {
			return "", err
		}
		if b > 1 && sb.Len() < maxLength {
			sb.WriteByte(b)
		} else {
			break
		}
	}
	return sb.String(), i.SetPosition(p + int64(sb.Len()))
}

func (i *IStorageAbstract) SetPosition(position int64) error {
	i.position = position
	return nil
}

func (i *IStorageAbstract) ReadByte() (byte, error) {
	bytes, err := i.ReadBytes(1)
	if err != nil {
		return 0, err
	}
	return bytes[0], nil
}

func (i *IStorageAbstract) ReadUint16() (uint16, error) {
	bytes, err := i.ReadBytes(2)
	if err != nil {
		return 0, err
	}
	return binary.LittleEndian.Uint16(bytes), nil
}

func (i *IStorageAbstract) ReadUint32() (uint32, error) {
	bytes, err := i.ReadBytes(4)
	if err != nil {
		return 0, err
	}
	return binary.LittleEndian.Uint32(bytes), nil
}

func (i *IStorageAbstract) ReadUint64() (uint64, error) {
	bytes, err := i.ReadBytes(8)
	if err != nil {
		return 0, err
	}
	return binary.LittleEndian.Uint64(bytes), nil
}

func (i *IStorageAbstract) ReadInt16() (int16, error) {
	readUint16, err := i.ReadUint16()
	if err != nil {
		return 0, err
	}
	return int16(readUint16), nil
}

func (i *IStorageAbstract) AsStorage() IStorage {
	return NewSubStorage(i, 0, i.Size())
}

func (i *IStorageAbstract) ReadInt64() (int64, error) {
	readUint64, err := i.ReadUint64()
	if err != nil {
		return 0, err
	}
	return int64(readUint64), err
}

func (i *IStorageAbstract) ReadAsciiZ() (string, error) {
	return i.ReadUtf8Z(math.MaxInt)
}

func (i *IStorageAbstract) ReadUTF8(length int) (string, error) {
	bytes, err := i.ReadBytes(length)
	if err != nil {
		return "", err
	}
	return string(bytes), nil
}

type SubStorage struct {
	IStorageAbstract
	offset    int64
	baseStore IStorage
	size      int64
}

func (i *SubStorage) Read(position int64, destination []byte) (int, error) {
	if position > i.Size() {
		return 0, errors.Errorf("position out of bounds: %d", i.Size())
	}
	return i.baseStore.Read(i.offset+position, destination)
}

func (i *SubStorage) Size() int64 {
	return i.size
}

func NewSubStorage(baseStore IStorage, offset, size int64) *SubStorage {
	s := &SubStorage{
		baseStore: baseStore,
		offset:    offset,
		size:      size,
	}
	s.SizeFunc = s.Size
	s.ReadFunc = s.Read
	return s
}

type FileStorage struct {
	IStorageAbstract
	size int64
	file *os.File
}

func (i *FileStorage) Close() error {
	return i.file.Close()
}

func (i *FileStorage) Read(position int64, destination []byte) (int, error) {
	if position > i.Size() {
		return 0, errors.Errorf("position out of bounds: %d", i.Size())
	}
	if _, err := i.file.Seek(position, io.SeekStart); err != nil {
		return 0, err
	}
	return i.file.Read(destination)
}

func (i *FileStorage) Size() int64 {
	return i.size
}

func NewFileStorage(filePath string) (*FileStorage, error) {
	file, err := os.OpenFile(filePath, os.O_RDONLY, os.ModePerm)
	if err != nil {
		return nil, err
	}
	stat, err := file.Stat()
	if err != nil {
		file.Close()
		return nil, err
	}
	reader := &FileStorage{
		file: file,
		size: stat.Size(),
	}
	reader.ReadFunc = reader.Read
	reader.SizeFunc = reader.Size
	return reader, nil
}

type MemoryStorage struct {
	IStorageAbstract
	data []byte
}

func (i *MemoryStorage) Read(position int64, destination []byte) (int, error) {
	if position > i.Size() {
		return 0, errors.Errorf("position out of bounds: %d", i.Size())
	}
	l := int64(len(destination))
	if position+l < i.Size() {
		copy(destination, i.data[position:position+l])
	} else {
		copy(destination, i.data[position:])
	}
	return int(l), nil
}

func (i *MemoryStorage) Size() int64 {
	return int64(len(i.data))
}

func NewMemoryStorage(data []byte) (storage *MemoryStorage) {
	storage = &MemoryStorage{
		data: data,
	}
	storage.IStorageAbstract.ReadFunc = storage.Read
	storage.IStorageAbstract.SizeFunc = storage.Size
	return
}
